plot() command is a generic function that (often) automatically adopts to data type.col for color) usually all have the same or similar names.help(functionname) or searching for the function by name in the Help tab on the right.Also, if you’ve been scrolling left and right in the script window to read the code, turn on text wrapping ASAP: on the menu bar above, go to Tools -> Global Options -> Code (on the left) -> tick “Soft-wrap R source files”.
So, print() is a function. Most functions look something like this:
myfunction(inputs, parameters)All the inputs to the function go inside the ( ) brackets, separated by commas. In the above case, the text is the input to the print() function. All text, or “strings”, must be within quotes. Note that commands may be nested; in this case, the innermost are evaluated first:
fun2( fun1(do, something), parameters ) # fun1 is evaluated first, and its output becomes the input for fun2.Don’t worry if that’s all a bit confusing for now. Let’s try another function, sum():
sum(1,10) # cursor on the line, press CTRL+ENTER (or CMD+ENTER on Mac)
# You should see the output (sum of 1 and 10) in the console.
# Important: you can always get help for a function and check its input parameters by executing
help(sum) # put the name of any function in the brackets
# ...or by searching for the function by name in the Help tab on the right.
# Exercise. You can also write commands directly in the console, and executing them with ENTER. Try some more simple maths - math in R can also be written using regular math symbols (which are really also functions). Write 2*3+1 in the console below, and press ENTER.
# Let's plot something. The command for plotting is, surprisingly, plot().
plot(42, main = "The greatest plot in the world") # execute the command; a plot should appear on the right.
# OK, that was not very exciting. But notice that a function can have multiple inputs, or arguments. In this case, the first argument is the data (a vector of length one), and the second is 'main', which specifies the main title of the plot.
# You can make to plot bigger by pressing the 'Zoom' button above the plot panel on the right.
# Let's create some data to play with. We'll use the sample() command, which creates random numbers from a predifined sample. Basically it's like rolling a dice some n times, and recording the results.
sample(x = 1:6, size = 50, replace = T) # execute this; its output is 50 numbers
# If an output is not assigned to some object, it usually just gets printed in the console. It would be easier to work with data, if we saved it in an object. For this, we need to learn assignement, which in R works using the equals = symbol (or the <-, but let's stick with = for simplicity).
dice = sample(x = 1:6, size = 50, replace = T) # what it means: xdata is the name of a (new) object, the equals sign (=) signifies assignement, with the object on the left and the data on the right. In this case, the data is the output of the sample() function. Instead of printing in the console, the output is assigned to the object.
dice # execute to inspect: calling an object usually prints its contents into the console below.
# Let's plot:
hist(dice, breaks=20, main="Frequency of dice values") # plots a histogram (distribution of values)
plot(dice) # plots data as it is ordered in the object
xmean = mean(dice) # calculate the mean of the 50 dice throws
abline(h = xmean, lwd=3) # plot the mean as a horizontal line
# Exercise: compare this plot with your neighbor. Do they look the same? Why/why not?
Numerical values include things we can measure on a continuous scale (height, weight, reaction time), things that can be ordered (“rate this on a scale of 1-5”), and things that have been counted (number of participants in an experiment, number of words in a text).
We will use a built-in classic dataset called “iris” - it contains information about a bunch of flowers.
data("iris") # load the data into the workspace (or "global environment").
# We can also inspect the data using R commands.
head(iris) # prints the first rows
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1 5.1 3.5 1.4 0.2 setosa
## 2 4.9 3.0 1.4 0.2 setosa
## 3 4.7 3.2 1.3 0.2 setosa
## 4 4.6 3.1 1.5 0.2 setosa
## 5 5.0 3.6 1.4 0.2 setosa
## 6 5.4 3.9 1.7 0.4 setosa
summary(iris) # produces an automatic summary of the columns
## Sepal.Length Sepal.Width Petal.Length Petal.Width
## Min. :4.300 Min. :2.000 Min. :1.000 Min. :0.100
## 1st Qu.:5.100 1st Qu.:2.800 1st Qu.:1.600 1st Qu.:0.300
## Median :5.800 Median :3.000 Median :4.350 Median :1.300
## Mean :5.843 Mean :3.057 Mean :3.758 Mean :1.199
## 3rd Qu.:6.400 3rd Qu.:3.300 3rd Qu.:5.100 3rd Qu.:1.800
## Max. :7.900 Max. :4.400 Max. :6.900 Max. :2.500
## Species
## setosa :50
## versicolor:50
## virginica :50
##
##
##
# In RStudio, you can also have a look at the dataframe by clicking on the little "table" icon next to it in the Environment section (top right).
help(iris) # built in datasets often have help files attached
# Plotting time! Let's see for example how long the petals are in the dataset
iris$Petal.Length # the $ is used for accessing (named) column of a dataframe
## [1] 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 1.5 1.6 1.4 1.1 1.2 1.5 1.3
## [18] 1.4 1.7 1.5 1.7 1.5 1.0 1.7 1.9 1.6 1.6 1.5 1.4 1.6 1.6 1.5 1.5 1.4
## [35] 1.5 1.2 1.3 1.4 1.3 1.5 1.3 1.3 1.3 1.6 1.9 1.4 1.6 1.4 1.5 1.4 4.7
## [52] 4.5 4.9 4.0 4.6 4.5 4.7 3.3 4.6 3.9 3.5 4.2 4.0 4.7 3.6 4.4 4.5 4.1
## [69] 4.5 3.9 4.8 4.0 4.9 4.7 4.3 4.4 4.8 5.0 4.5 3.5 3.8 3.7 3.9 5.1 4.5
## [86] 4.5 4.7 4.4 4.1 4.0 4.4 4.6 4.0 3.3 4.2 4.2 4.2 4.3 3.0 4.1 6.0 5.1
## [103] 5.9 5.6 5.8 6.6 4.5 6.3 5.8 6.1 5.1 5.3 5.5 5.0 5.1 5.3 5.5 6.7 6.9
## [120] 5.0 5.7 4.9 6.7 4.9 5.7 6.0 4.8 4.9 5.6 5.8 6.1 6.4 5.6 5.1 5.6 6.1
## [137] 5.6 5.5 4.8 5.4 5.6 5.1 5.1 5.9 5.7 5.2 5.0 5.2 5.4 5.1
iris[, "Petal.Length"] # this is the other indexing notation: [row, column]
## [1] 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 1.5 1.6 1.4 1.1 1.2 1.5 1.3
## [18] 1.4 1.7 1.5 1.7 1.5 1.0 1.7 1.9 1.6 1.6 1.5 1.4 1.6 1.6 1.5 1.5 1.4
## [35] 1.5 1.2 1.3 1.4 1.3 1.5 1.3 1.3 1.3 1.6 1.9 1.4 1.6 1.4 1.5 1.4 4.7
## [52] 4.5 4.9 4.0 4.6 4.5 4.7 3.3 4.6 3.9 3.5 4.2 4.0 4.7 3.6 4.4 4.5 4.1
## [69] 4.5 3.9 4.8 4.0 4.9 4.7 4.3 4.4 4.8 5.0 4.5 3.5 3.8 3.7 3.9 5.1 4.5
## [86] 4.5 4.7 4.4 4.1 4.0 4.4 4.6 4.0 3.3 4.2 4.2 4.2 4.3 3.0 4.1 6.0 5.1
## [103] 5.9 5.6 5.8 6.6 4.5 6.3 5.8 6.1 5.1 5.3 5.5 5.0 5.1 5.3 5.5 6.7 6.9
## [120] 5.0 5.7 4.9 6.7 4.9 5.7 6.0 4.8 4.9 5.6 5.8 6.1 6.4 5.6 5.1 5.6 6.1
## [137] 5.6 5.5 4.8 5.4 5.6 5.1 5.1 5.9 5.7 5.2 5.0 5.2 5.4 5.1
plot(iris$Petal.Length) # two observations: there is quite a bit of variation, and it seems there are clusters in the data
hist(iris$Petal.Length, breaks=10) # a histogram shows the distribution of values ('breaks' change resolution)
boxplot(iris$Petal.Length) # a boxplot is like a visual summary()
points(x=rep(1, nrow(iris)), y=iris$Petal.Length) # could also add actual datapoints
# Bonus: an easy way to deal with overlapping points is to add noise in the dimension that is not informative anyway:
boxplot(iris$Petal.Length); points(x=jitter(rep(1, nrow(iris)),5), y=iris$Petal.Length)
# Here's something to try: the default color of the points is black. Change it to something else by adding the parameter col to the points command (remember, parameters are separated by commas, and they are given values using the = sign; color names must be in quotes, e.g., "red").
# Plot boxplots, grouped by the Species variable:
boxplot(iris$Petal.Length ~ iris$Species) # note the ~ notation
grid() # why not add a grid for reference
# A slightly nicer version:
boxplot(iris$Petal.Length ~ iris$Species, ylab="petal length",
border=c("plum3", "darkblue", "lightblue"), boxwex=0.7, cex=0.4)
abline(h=1:7, col=rgb(0,0,0,0.1)) # adds vertical lines instead of full grid
A Note on the rgb(red, green, blue, alpha) function: this allows making custom colors; alpha controls transparency. Possible values range between 0 and 1 by default. Below is a piece of code that generates an example of how the color scheme works (don’t worry if you don’t understand the actual code, this is above the level of this workshop; just put the cursor in the code block and press CTRL+SHIFT+ENTER (CMD+SHIFT+ENTER on Mac).
plot(iris$Sepal.Length, iris$Sepal.Width) # no interaction?
# Why not color-code by species. Here we make use of both styles of indexing.
iriscolors= c(rgb(0.5,0,0.9, 0.6), rgb(0,0,0.6,0.6), rgb(0,0.7,1,0.6)) # c("purple", "darkblue", "lightblue") but with transparency
irispoints = c(15,16,17) # see help(points) for more
plot(iris$Sepal.Length, iris$Sepal.Width,
col=iriscolors[iris$Species], pch=20) # pch sets the point type
grid(col=rgb(0,0,0,0.3), lty=3)
# This is suddenly a lot of code out of nowhere... If some of it looks overwhelming, worry not! Everything will become clear once you get into R a bit more.
# Add some detail to make this legible to the colour-blind, and printable in black-and-white
irispoints = c(15,20,17) # see help(points) for more
plot(iris$Sepal.Length, iris$Sepal.Width, col = iriscolors[iris$Species] , pch = irispoints[iris$Species])
# Make this publication-ready by adding proper labels and a legend
plot(iris$Sepal.Length, iris$Sepal.Width,
col=iriscolors[iris$Species] , pch=irispoints[iris$Species],
main="Iris sepals in three species", xlab="Sepal length", ylab="Sepal width")
grid(col=rgb(0, 0, 0, 0.2)) # 0,0,0 results in black, 0.2 alpha makes it transparent (so, grey on a white background)
legend("topleft", pch=irispoints, legend = levels(iris$Species), col=iriscolors, cex=0.7, bty="n")
While a whole subject on its own, we will have a quick look at plotting time series - data reflecting changes in some variable over time.
# This time we'll generate some random data and pretend it's real data.
# "The following data are reaction times to stimuli of one individual, over 100 trials, in an experiment on...whatever"
retime = c(runif(20, 0,0.1), seq(0.1, 2.8,length.out = 80)) * runif(100, 0.7, 1.1 )
# Have a look at the raw data first! (by now you already know how to do it)
# Now let's plot it
plot(retime, ylab="reaction time") # this plots points though
What can you tell by the looks of the data?
Exercise: improve this plot by adding the type parameter and setting its value as “l” (which stands for ‘line’, which is more useful in this instance), and set the X axis label (xlab) to say “trials” instead of the default “Index”.
Categorical/nominal/discrete values cannot be put on a continuous scale or ordered, and include things like binary values (student vs non-student) and all sorts of labels (noun, verb, adjective). Words in a text could be viewed as categorical data.
Here is another artificial dataset. Let’s pretend I went around Edinburgh and asked random people on the street the following question: “A new species of insect was recently discovered in Scotland, and they called it Boubicus Boubasus - or Bouba for short. What’s your intuition, is Bouba a big fat bug, or a small slim bug?” (and the same for Kikis Kikosius, or Kiki for short)
boubakiki = data.frame(
meanings=c(
sample(c("big", "small"),25,T, prob=c(0.8,0.2)),
sample(c("big", "small"),24,T, prob=c(0.3,0.7))
),
words=c(rep("bouba",25), rep("kiki",24))
) # this command will create the random data
# Have a look at the raw data first.
# In addition to eyeballing the data, use the following commands: nrow(), dim()
# Now let's use the table() function to make sense of it:
bktable = table(boubakiki)
bktable
## words
## meanings bouba kiki
## big 19 8
## small 6 16
mosaicplot(bktable, col=c("orange", "navy")) # a simple mosaic plot, displays proportions
barplot(bktable, ylab="big small") # a barplot, displays counts
install.packages("wordcloud") # install a "package", a collection of functions to extend R's basic functionality; this needs to be done only once for each package.
# Let's create an object with a bunch of text:
sometext = "In a hole in the ground there lived a hobbit. Not a nasty, dirty, wet hole, filled with the ends of worms and an oozy smell, nor yet a dry, bare, sandy hole with nothing in it to sit down on or to eat: it was a hobbit-hole, and that means comfort. It had a perfectly round door like a porthole, painted green, with a shiny yellow brass knob in the exact middle. The door opened on to a tube-shaped hall like a tunnel: a very comfortable tunnel without smoke, with panelled walls, and floors tiled and carpeted, provided with polished chairs, and lots and lots of pegs for hats and coats—the hobbit was fond of visitors. The tunnel wound on and on, going fairly but not quite straight into the side of the hill — The Hill, as all the people for many miles round called it — and many little round doors opened out of it, first on one side and then on another. No going upstairs for the hobbit: bedrooms, bathrooms, cellars, pantries (lots of these), wardrobes (he had whole rooms devoted to clothes), kitchens, dining-rooms, all were on the same floor, and indeed on the same passage. The best rooms were all on the left-hand side (going in), for these were the only ones to have windows, deep-set round windows looking over his garden, and meadows beyond, sloping down to the river."
# Now let's do some very basic preprocessing to be able to work with the words in the text:
clean = gsub("[[:punct:]]", "", sometext) # remove punctuation (that weird thing inside the gsub (R's find-and-replace command) is a regular expression; don't ask, it just works)
cleanlow = tolower(clean) # make everything lowecase
words = strsplit(cleanlow, split=" ")[[1]]
# Inspect the object we just created. It should be a vector of 232 words.
# Some ways to inspect and visualize textual data
sortedwords = sort(table(words)) # counts the words and sorts them
# Exercise: have a look at the data using the head() and tail() commands
plot(sortedwords, xaxt="n")
axis(1, 1:length(sortedwords), names(sortedwords), las=2, cex.axis=0.5) # add the words
# Time to use the wordcloud package we installed earlier
library("wordcloud") # load the package (needs to be done again when you start R again)
## Loading required package: RColorBrewer
wordfreqs = as.numeric(sortedwords) # get the frequencies from the table object
wordcloud(words = names(sortedwords), freq=wordfreqs, min.freq = 0) # needs the wordcloud package to work
# Note: if R gives you errors (saying word x could not fit), ignore them. Also, if plots look strange after using wordcloud, use the dev.off() command to reset graphics.
In the following examples, we’ll employ some light corpus analysis tools to visualize the content of the inaugural speeches of US presidents. We’ll start by looking into which presidents mention or address other presidents in their speeches.
library("quanteda", quietly=T, warn.conflicts=F) # make sure this is installed and load it; this also includes a dataset
## Package version: 1.2.0
## Parallel computing: 2 of 4 threads used.
## See https://quanteda.io for tutorials and examples.
library("igraph", quietly=T, warn.conflicts=F)
library("visNetwork", quietly=T, warn.conflicts=F)
speeches = data_corpus_inaugural$documents$texts # extract speeches data from the internal object
speeches = gsub("Washington DC", "DC", speeches) # replace city name to avoid confusion with president Washington (hopefully)
speechgivers = data_corpus_inaugural$documents$President # names of presidents giving the speech
presidents = unique(data_corpus_inaugural$documents$President) # presidents (some were elected more than once)
# Exercise: have a look at speech number 58, and check who's giving the speech. Hint: use the bracket [] notation
# The following piece of code looks for names of presidents in the speeches using grep(). Just run this little block:
mentions = matrix(0, ncol=length(presidents), nrow=length(presidents), dimnames=list(presidents, presidents))
for(president in presidents){
foundmentions = grep(president, speeches)
mentions[speechgivers[foundmentions], president ] = 1
}
# Note: this is not perfect - the code above concatenates mentions of multiple speeches by the same re-elected president, "Bush" refers to two different people, and other presidents might share names with other people as well. You can check the context of keywords using quanteda's kwic() command:
kwic(data_corpus_inaugural, "Monroe")
##
## [1885-Cleveland, 1202] It is the policy of | Monroe |
## [1909-Taft, 1784] bears the name of President | Monroe |
## [1925-Coolidge, 494] , and secured by the | Monroe |
##
## and of Washington and Jefferson
## . Our fortifications are yet
## doctrine. The narrow fringe
#
# Have a look at the data some basic stats:
mentions[30:35, 30:35] # rows: one mentioning; columns: being mentioned
## Carter Reagan Bush Clinton Obama Trump
## Carter 0 0 0 0 0 0
## Reagan 0 0 1 0 0 0
## Bush 1 1 1 1 0 0
## Clinton 0 0 1 0 0 0
## Obama 0 0 1 0 0 0
## Trump 1 0 1 1 1 0
counts = apply(mentions, 2, sum)
barplot(counts, horiz = T, las=1) # number of mentions
# Plotting time
pgraph = graph_from_adjacency_matrix(mentions, mode="directed") # this uses igraph
plot(pgraph, edge.arrow.size=0.4) # basic igraph plot
# this uses visNetwork:
pgraph_v = toVisNetworkData(pgraph )
v = visNetwork(nodes = pgraph_v$nodes, edges = pgraph_v$edges)
v # check how it looks before we add all the fancy stuff
v = visNodes(v, size = 10, shadow=T, font=list(size = 30))
v = visIgraphLayout(v, "layout_in_circle", smooth=T)
v = visEdges(v, arrows = "to", shadow=T, smooth=list(type="discrete"), selectionWidth=5)
v = visOptions(v, highlightNearest = list(enabled = T, hover = T, degree=1, labelOnly=F, algorithm="hierarchical"), nodesIdSelection = T)
v
While we’re at it, let’s try to probe into the contents of the speeches and use some more interactive plotting tools to visualize it.
library("quanteda", quietly = T, warn.conflicts=F) # this needs to be loaded
library("plotly", quietly = T, warn.conflicts=F) # this too
# This block of code will extract the top terms (weighted by tfidf) from the most recent speeches and calculate the distance between the speeches based on word usage
termmat = dfm_tfidf(dfm(corpus_subset(data_corpus_inaugural, Year>1990), tolower = T, stem=F,remove=stopwords(), remove_punct=T))
topterms = lapply(topfeatures(termmat, n=10, groups=rownames(termmat)), names)
distmat = dist(termmat) # calculate distances
mds = as.data.frame(cmdscale(distmat,k = 2)) # multidimentsional scaling (reduces distance matrix to 2 dimensions)
mds$tags = paste(names(topterms), sapply(topterms, paste, collapse="<br>"), sep="<br>")
# The following makes use of the plotly package
p = plot_ly(mds,x=~V1,y=~V2, type="scatter", mode = 'markers', hoverinfo = 'text', text = ~tags)
p = add_annotations(p, text = ~rownames(mds), xanchor="left", showarrow = F)
p # closer points mark more similar speeches; hover to see key terms that distinguish the speeches
# A look into the usage of some words across centuries
termmat_all = dfm_weight(dfm(data_corpus_inaugural, tolower = T, stem=F,remove=stopwords(), remove_punct=T), "prop") # use normalized frequencies
words = c("america", "states", "dream", "hope", "business", "peace", "war", "terror")
p = plot_ly(x=words, y=rownames(termmat_all), z=round(as.matrix(termmat_all[,words]),5), type="heatmap", colors = colorRamp(c("white", "orange", "darkred")),showscale = F)
p = layout(p, margin = list(l=130, b=50), paper_bgcolor=rgb(0.99, 0.98, 0.97))
p
# Exercise: choose some other words!
That’s it for today. Do play around with these things later when you have time, and look into the bonus sections for more cool stuff. If you get stuck, Google is your friend; also, check out www.stackoverflow.com - this site is a goldmine of programming (including R) questions and solutions. If you get into making more plots, you might also want to look into the ggplot2 package, which offers an alternative way of making plots, which some people find more intuitive than the basic R way.
Also, if you get around analysing your own data and need help in terms of writing about the results of your analyses and presenting your data, choosing the right graph type, what to plot, and in general how to plot the things you want to plot - feel free to book a session with me through the PPLS Writing Centre: go to http://writingcentre.ppls.ed.ac.uk/appointments/ -> Book an appointment -> look for my name (Andres Karjus) in the list of tutors, in the ‘Select staff’ dropdown menu.
But wait! There’s one more thing to do. Since this is an R Markdown document, we can “knit” it into a nice HTML (or PDF, or Word) report file - it will show both the code and the plots produced by the code. Note that unfortunately this will not work if you have errors in your code - marked by the little red x signs on the left side vertical bar. To knit, click the Knit button (with the little blue ball of yarn) above the script window. If the code is without errors, an HTML document will appear.
Here are some more things you can try out at home later.
Small note: if you try knitting the RMarkdown file again later and would like to see output from the bonus sections, set eval=TRUE in these blocks, which will allow them to be rendered (all bonus blocks currently have the eval parameter set as FALSE). Do not set eval to TRUE in the small blocks with the install.packages() commands though. You might have also noticed the echo=F parameter - this just means the code itself will not be rendered in the knit output (even when it is executed).
A quick look at a package that creates interactive (clickable, rotatable, etc) plots, both in 2D and 3D - these also work in web pages (like the html file you could create by knitting this script file; R Markdown can also be used to create slides, meaning you could easily include interactive graphs in your next presentation).
Making maps programmatically based on data would come in handy if your worked with demographic data, or dialects, areal sociolinguistics, etc.
Once you get around to working with your own data, you’ll need to import it into R to be able to make plots based on it. There are a number of ways of doing that.
This is probably the most common use case. If your data is in an Excel file formal (.xls, .xlsx), you are better off saving it as a plain text file (although there are packages to import directly from these formats, as well as from SPSS .sav files). The commands for that are read.table(), read.csv() and read.delim(). They basically all do the same thing, but differ in their default settings.
There is a simple way to import data from the clipboard. While importing from files is generally a better idea (you can always re-run the code and it will find the data itself), sometimes this is handy, like quickly grabbing a little piece of table from Excel. It differs between OSes:
For text, the readLines() command usually works well (its output is a character vector, so if the text file has 10 lines, then readLines produces a vector of length 10, where each line is an element in that vector (you could use strsplit() to further split it into words. If the text is organized neatly in columns, however, you might still consider read.table(), but probably with the stringsAsFactors=F parameter (this avoids making long text strings into factors, read up on it if needed).
RStudio has handy options to export plots - click on Export on top of the plot panel, and choose the output format. Plots can be exported using R code as well - this is in fact a better approach, since otherwise you would have to click through the Export… menus again every time you change your plot and need to re-export. Look into the help files of the jpeg and pdf functions to see how this works.
There are a number of ways for creating animated plots in R and making nice GIFs that you can use in a talk, on your website or wherever. There is the animation package, and plotly supports animations; or on a Mac you can use ImageMagick’s Terminal commands to convert any plot files into a GIF (you can send commands to Mac’s Terminal using the system() command; learn about loops to easily generate a number of plots with only a few lines of code).
There are also packages to import (and manipulate) images, GIS map data (shapefiles), data from all sorts of other file formats (like XML, HTML) and many more. Just google a bit and you’ll find what you need.
R and its packages are all free open-source software, meaning countless people have invested a lot of their own time into making this possible. If you use R, do cite it in your work (use the handy citation() command in R to get an up to date reference, both in plain text and BibTeX). To cite a package, use citation("package name"). You are also absolutely welcome to use any piece of code from this workshop, but in that case I would likewise appreciate a citation:
BibTeX:
@misc{karjus_artofthefigure_2018,
author = {Karjus, Andres},
title = {aRt of the Figure},
year = {2018},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/andreskarjus/artofthefigure}},
DOI = {10.5281/zenodo.1213335}
}
Social networks
The following example will look into plotting social networks based on who knows who.
Let’s try something else. Using the same graph data, we’ll recreate it using another package, visNetwork, which makes graphs interactable.